package com.virtual.repository.plugins.mongo;

import java.net.UnknownHostException;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.ServerAddress;

import com.virtual.repository.common.criteria.Criterion;
import com.virtual.repository.common.operators.CriteriaOperator;
import com.virtual.repository.common.queries.Query;
import com.virtual.repository.common.queries.ResultEntity;
import com.virtual.repository.plugins.api.IDatabasePlugin;

/**
 * A driver (database plugin) for the MongoDB database.
 * @author Zielony
 * @version 1.0
 */
public class MongoDBDriver implements IDatabasePlugin {
	/**
	 * The ">" operator.
	 */
	private static final String GT = "$gt";
	
	/**
	 * The ">=" operator.
	 */
	private static final String GE = "$ge";
	
	/**
	 * The "<" operator.
	 */
	private static final String LT = "$lt";
	
	/**
	 * The "<=" operator.
	 */
	private static final String LE = "$le";
	
	/**
	 * The "IN" operator.
	 */
	private static final String IN = "$in";
	
	/**
	 * The "NOT IN" operator.
	 */
	private static final String NIN = "$nin";
	
	/**
	 * The "<>" operator.
	 */
	private static final String NE = "$ne";
	
	/**
	 * The "LIKE" operator.
	 */
	private static final String REGEX = "$regex";
	
	/**
	 * The username for DB connection.
	 */
	public String username;
	
	/**
	 * The password for DB connection.
	 */
	public String password;
	
	/**
	 * The name of the database to be accessed.
	 */
	public String dbName;
	
	/**
	 * The underlying MongoDB connection.
	 */
	private Mongo connection;
	
	/**
	 * The address of the MongoDB server to be connected.
	 */
	private String serverAddress;
	
	/**
	 * The port on the server on which the MongoDB is running.
	 */
	private int serverPort;
	
	/**
	 * Currently sued database.
	 */
	private DB db;
	
	public MongoDBDriver() {
		//needed for initialization in AbstractRepositoryManager
	}
	
	/**
	 * A constructor fully initializing the driver.
	 * @param serverAddress the address of the MongoDB server station.
	 * @param serverPort the port on which the MongoDB server is running.
	 * @param username the name of the user to connect.
	 * @param password the password to connect.
	 * @param dbName the name of the database to be accessed.
	 */
	public MongoDBDriver(String serverAddress, int serverPort, String username, String password, String dbName) {
		/*
		 * A null serverAddress is not allowed.
		 */
		if(serverAddress == null) {
			throw new IllegalArgumentException("Attempt to create a MongoDB connection to a null server address");
		}
		
		/*
		 * A connection with a password and no username is not allowed, while neither username nor password,
		 * username without password or with both username and password are all allowed.
		 */
		if(password != null && username == null) {
			throw new IllegalArgumentException("Attempt to create a MongoDB connection with a password but no username");
		}
		
		/*
		 * A null database name is not allowed. 
		 */
		if(dbName != null) {
			throw new IllegalArgumentException("Attempt to connect to an unnamed database");
		}
		
		/*
		 * Setting the initial driver state.
		 */
		this.serverAddress = serverAddress;
		this.serverPort = serverPort;
		this.username = username;
		this.password = password;
		this.dbName = dbName;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void connect() throws UnknownHostException {
		/*
		 * Nawiazuje polaczenie z baza wg podanych parametr�w. 
		 */
		connection = new Mongo(new ServerAddress(serverAddress, serverPort)); 
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Collection<ResultEntity> executeQuery(Query query) {
		/*
		 * bulding the final result collection.
		 */
		Collection<ResultEntity> finalResult = new LinkedList<ResultEntity>();
		
		/*
		 * Parsing the general Query to a MongoDB-native DBObject.
		 */
		DBObject mongoQuery = parseQuery(query);
		
		/*
		 * Accessing a particular database.
		 */
		db = connection.getDB(dbName);
		
		/*
		 * Accessing a particular collection using the entity name.
		 */
		DBCollection dbCollection = db.getCollection(query.getEntityName());
		
		/*
		 * Finding the results for a query.
		 */
		DBCursor c = dbCollection.find(mongoQuery);
	
		/*
		 * The i-th result from the cursor.
		 */
		ResultEntity iResult;
		
		/*
		 * Parsing all results consecutively, wrapping the into ResultEntities and adding to the finalResult list.
		 */
		while(c.hasNext()) {
			
			iResult = new ResultEntity();
			iResult.setEntityFields(parseResult(c.next()));
			finalResult.add(iResult);
		}

		return finalResult;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public ResultEntity executeQueryForSingleResult(Query query) {
		/*
		 * Parsing the query from the Query format to the DBObject native to MongoDB.
		 */
		DBObject mongoQuery = parseQuery(query);
		
		/*
		 * Accessing a particular database.
		 */
		db = connection.getDB(dbName);
		
		/*
		 * Accessing a collection by the entity name.
		 */
		DBCollection dbCollection = db.getCollection(query.getEntityName());
		
		/*
		 * Getting a single result for the given query.
		 */
		DBObject c = dbCollection.findOne(mongoQuery);		

		/*
		 * Packing the result as ResultEntity 
		 */
		ResultEntity result = new ResultEntity();
		result.setEntityFields(parseResult(c));
		
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public long executeQueryForCount(Query query) {
		/*
		 * Parsing the general Query to MongoDB-native DBObject.
		 */
		DBObject mongoQuery = parseQuery(query);
		
		/*
		 * Accessing a particular database.
		 */
		db = connection.getDB(dbName);
		
		/*
		 * Accessing a particular connection using the entityName. 
		 */
		DBCollection dbCollection = db.getCollection(query.getEntityName());
		
		/*
		 * Returning the result count.
		 */
		return dbCollection.count(mongoQuery);
	}
	
	/**
	 * Parser the general <code>Query</code> to the MongoDB-native <code>DBObject</code> format.
	 * @param query database-independent query.
	 * @return the MongoDb-native <code>DBObject</code> query mapping.
	 */
	private DBObject parseQuery(Query query) {
		/*
		 * Creating the native query.
		 */
		BasicDBObject mongoQuery = new BasicDBObject();
		
		/*
		 * An iterator to browse the selection criteria.
		 */
		Iterator<Criterion> i = query.getSelectionCriteria().iterator();
		
		/*
		 * The i-th selection criterion.
		 */
		Criterion iCriterion;
		
		/*
		 * The i-th criterion operator.
		 */
		CriteriaOperator iCriteriaOperator;
		
		//TODO: The NOT_LIKE operator.
		//TODO: the NOT_LIKE operator not supported in MongoDB.
		
		while(i.hasNext()) {
			/*
			 * Fetching the criterion and the operator.
			 */
			iCriterion = i.next();
			iCriteriaOperator = iCriterion.getCriteriaOperator();
			
			/*
			 * Translating the operators to the mongoDB format.
			 */
			if(iCriteriaOperator.equals(CriteriaOperator.EQUALS)) {
				mongoQuery.append(iCriterion.getProperty(), iCriterion.getValue());
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.GRATER)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(GT, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.GRATER_OR_EQUAL)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(GE, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.LESSER)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(LT, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.LESSER_OR_EQUAL)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(LE, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.IN)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(IN, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.NOT_EQUAL)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(NE, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.NOT_IN)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(NIN, iCriterion.getValue()));
			}
			else if(iCriteriaOperator.equals(CriteriaOperator.LIKE)) {
				mongoQuery.append(iCriterion.getProperty(),
						new BasicDBObject().append(REGEX, iCriterion.getValue()));
			}
		}
		
		return mongoQuery;
	}
	
	/**
	 * Presents the <code>result</code> of a query in a <code>Map</code> form.
	 * @param result the database query result as a <code>DBObject</code>.
	 * @return a <code>Map</code> form of the <code>result</code>.
	 */
	private Map<String, Object> parseResult(DBObject result) {
		/*
		 * The returned object.
		 */
		Map<String, Object> parsedResult = new TreeMap<String, Object>();
		
		/*
		 * An iterator to browse through the key names in the result set..
		 */
		Iterator<String> fieldNameIterator = result.keySet().iterator();
		
		/*
		 * The name of the i-th field in the result set.
		 */
		String iFieldName;
		
		/*
		 * The value of the i-th field in the result set.
		 */
		Object iValue;
		
		while(fieldNameIterator.hasNext()) {
			/*
			 * Retrieving the i-th field name.
			 */
			iFieldName = fieldNameIterator.next();
			
			/*
			 * Retrieving the i-th field value.
			 */
			iValue = result.get(iFieldName);
			
			/*
			 * If the retrieved object is compound, parsing it recursively and inserting into the map.
			 */
			if(DBObject.class.isAssignableFrom(iValue.getClass())) {
				Map<String, Object> subobjectAsMap = parseResult((DBObject)iValue);
				
				parsedResult.put(iFieldName, subobjectAsMap);
			}
			/*
			 * If the retrieved object is not compound (hence a primitive, date etc.), inserting it directly.
			 */
			else {
				parsedResult.put(iFieldName, iValue);
			}
		}
		
		return parsedResult;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void disconnect() {
		this.connection.close();
	}

	/**
	 * Gets: the username.
	 * @return the username.
	 */
	public String getUsername() {
		return username;
	}

	/**
	 * Sets: the username. 
	 * @param username the username.
	 */
	public void setUsername(String username) {
		this.username = username;
	}

	/**
	 * Gets: the password.
	 * @return the password.
	 */
	public String getPassword() {
		return password;
	}

	/**
	 * Sets: the password.
	 * @param password the password.
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * Gets: the database name.
	 * @return the database name.
	 */
	public String getDbName() {
		return dbName;
	}

	/**
	 * Sets: the database name.
	 * @param dbName the database name.
	 */
	public void setDbName(String dbName) {
		this.dbName = dbName;
	}

	/**
	 * Gets: the MongoDB server address.
	 * @return the MongoDB server address.
	 */
	public String getServerAddress() {
		return serverAddress;
	}

	/**
	 * Sets: the MongoDB server address.
	 * @param serverAddress the MongoDB server address.
	 */
	public void setServerAddress(String serverAddress) {
		this.serverAddress = serverAddress;
	}

	/**
	 * Gets: the port number MongoDB is running on.
	 * @return the port number MongoDB is running on.
	 */
	public int getServerPort() {
		return serverPort;
	}

	/**
	 * Sets: the port number MongoDB is running on.
	 * @param serverPort the port number MongoDB is running on.
	 */
	public void setServerPort(int serverPort) {
		this.serverPort = serverPort;
	}
}